/* ----------------------------------------------------------------------
 *	Shadow Quest
 *	Copyright (C) 2009 Prageeth Silva <prageeth@thenewcoders.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *  ----------------------------------------------------------------------
 */
package map;

import java.io.IOException;
import java.util.ArrayList;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;

import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.tiled.TiledMap;
import org.newdawn.slick.util.pathfinding.AStarPathFinder;
import org.newdawn.slick.util.pathfinding.PathFinder;
import org.newdawn.slick.util.pathfinding.PathFindingContext;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
import org.xml.sax.SAXException;

import panel.Panel;

import sq.FontLoader;
import sq.RPG;
import sq.SoundLoader;
import sq.FileIO;
import sq.game.Cheats;
import sq.game.Quest;
import units.Player;
import units.Unit;

/** Represents the entire game world.
 * (Designed to be instantiated just once for the whole game).
 */
public class World implements TileBasedMap {

    // the position of the player where teleporting should stop at.
    public static float TELEPORT_X = 900, TELEPORT_Y = 590;
    public static final float CAMERA_SLIDE_SPEED_X = 0.01f, CAMERA_SLIDE_SPEED_Y = 0.01f;
    public static final int MAX_DISTANCE = 5;
    public static final int TERRAIN_ANY = -1, TERRAIN_OPEN = 0, TERRAIN_CAVE = 1, TERRAIN_WATER = 2, TERRAIN_OBSTACLE = 3;
    // strings that define the blocked property of a given tile
    private static final String BLOCK_PROPERTY = "block", TILE_BLOCKED = "1", TILE_UNBLOCKED = "0";
    private static final String TERRAIN_PROPERTY = "terrain", DEFAULT_TERRAIN = String.valueOf(TERRAIN_ANY);
    private static final String COLOR_PROPERTY = "color", DEFAULT_COLOR = "t";
    private TiledMap map;						// the map details of the world
    private float cameraX, cameraY;			// camera position, focusing on the given point (center of the screen)
    private boolean blockedTiles[][];			// cache of which tiles are blocked
    private int tileTerrain[][];				// cache of which tiles are blocked
    private Color tileColors[][];				// cache of the tile colors used for the minimap
    private PathFinder finder;					// the path finder
    private Weather weather;					// the weather generator
    private Player player;						// the player sprite
    private ArrayList<Item> items;					// the items lieing around in the world
    private ArrayList<OtherObject> objects;		// the uninteractive objects in the world
    private ArrayList<Unit> units;
    private ArrayList<Unit> allUnits;
    private ArrayList<Unit> dead;
    private Quest quest;

    /** Create a new World object
     * @param initx the initial x coordinate of the player
     * @param inity the initial y coordinate of the player
     * @param screenWidth the width of the display screen
     * @param screenHeight the height off the display screen
     */
    public World(Quest quest)
            throws SlickException {

        this.quest = quest;

        // initialise the collections
        this.items = new ArrayList<Item>();
        this.units = new ArrayList<Unit>();
        this.allUnits = new ArrayList<Unit>();
        this.dead = new ArrayList<Unit>();
        this.objects = new ArrayList<OtherObject>();


        // the game starts
        this.weather = new Weather(this, 3);

    }

    public void loadMap(String map, String pathTiles)
            throws SlickException {
        // create the world
        this.setMap(map, pathTiles);

        // create the blocked cache array
        this.blockedTiles = new boolean[this.getMap().getWidth()][this.getMap().getHeight()];
        this.tileColors = new Color[this.getMap().getWidth()][this.getMap().getHeight()];
        this.tileTerrain = new int[this.getMap().getWidth()][this.getMap().getHeight()];

        // cache the blocked tiles, tile colors
        this.cacheMapDetails();

        this.finder = new AStarPathFinder(this, MAX_DISTANCE, false);

    }

    /** Update the game state for a frame.
     * @param input The input object, used to take the keyboard input.
     * @param delta Time passed since last frame (milliseconds).
     */
    public void update(Input input, int delta)
            throws SlickException {
        // Toggle cheats
        boolean cheats = input.isKeyPressed(Input.KEY_F8);
        if (cheats) {
            if (Cheats.isOnGodMode()) {
                Cheats.removeGodMode(this.getPlayer());
            } else {
                Cheats.applyGodMode(this.getPlayer());
            }
        }

        // update the weather
        this.getWeatherGen().update(this, delta, this.getCameraX(), this.getCameraY());

        // update the items
        for (Item i : this.getItems()) {
            i.update(this, delta);
        }

        // update all the units
        for (Unit u : this.getAllOtherUnits()) {
            if (u.isOnMap()) {
                u.update(input, delta);
            }
        }

        // update the objects
        for (OtherObject o : this.getObjects()) {
            o.update(this, delta);
        }

        // update the dead units collection
        int length = this.getDeadUnitsOnly().size() - 1;
        for (int i = length; i >= 0; i--) {
            if (!this.getDeadUnitsOnly().get(i).isOnMap()) {
                this.getDeadUnitsOnly().remove(this.getDeadUnitsOnly().get(i));
            }
        }

        // update player graphics and update the camera position
        this.getPlayer().update(input, delta);
        this.moveCameraTo((float) this.getPlayer().getX(), (float) this.getPlayer().getY());
//    	this.moveCameraTo(1500,200);
        //this.slideCameraTo(this.getPlayer().getX(),this.getPlayer().getY(), delta); // testing

    }

    /** Render the entire screen, so it reflects the current game state.
     * @param g The Slick graphics object, used for drawing.
     */
    public void render(Graphics g)
            throws SlickException {
        // the offset of the map depending on the camera position
        int tileOffsetX = (int) -(this.getCameraX() % this.getMap().getTileWidth());
        int tileOffsetY = (int) -(this.getCameraY() % this.getMap().getTileHeight());

        // the left most tile's index to be shown
        int tileIndexX = (int) (this.getCameraX() / this.getMap().getTileWidth());
        int tileIndexY = (int) (this.getCameraY() / this.getMap().getTileHeight());

        // render the map and then the player
        this.getMap().render(tileOffsetX, tileOffsetY, tileIndexX, tileIndexY,
                (RPG.getScreenWidth()) / this.getMap().getTileWidth() - tileOffsetX + 1,
                (RPG.getScreenHeight()) / this.getMap().getTileHeight() - tileOffsetY + 1);

        //render dead units
        for (Unit u : this.getDeadUnitsOnly()) {
            if (u.isOnMap() && !u.isAlive()) {
                u.render(g, this.getCameraX(), this.getCameraY());
            }
        }

        //render the items which have NO exploding animations
        for (Item i : this.getItems()) {
            if (i.isOnMap() && !i.isDestroyed()) {
                i.renderOnMap(g, this.getCameraX(), this.getCameraY());
            }
        }

        //render the objects
        for (OtherObject o : this.getObjects()) {
            if (o.isOnMap()
                    && (o.isDestroyed() || (o.getTravelType() != OtherObject.TravelType.AIR))) {
                o.render(g, this.getCameraX(), this.getCameraY());
            }
        }

        //render the all other units
        for (Unit u : this.getAllOtherUnits()) {
            if (u.isOnMap() && u.isAlive() && !u.isFlyingUnit()) {
                u.render(g, this.getCameraX(), this.getCameraY());
            }
        }

        // render the player
        Player p = this.getPlayer();
        p.render(g, this.getCameraX(), this.getCameraY());

        // render the display names, health bars, dialogues, misses of other units
        for (Unit u : this.getAllOtherUnits()) {
            if (u.isAlive() && u.isOnMap() && !u.isFlyingUnit()) {
                if (Unit.isVillager(u)) {
                    u.renderDisplayNameOnly(g, this.getCameraX(), this.getCameraY());
                } else {
                    u.renderDisplayNameAndHP(g, this.getCameraX(), this.getCameraY());
                }
                u.renderMisses(g, this.getCameraX(), this.getCameraY());
                u.renderDialogue(g, this.getCameraX(), this.getCameraY());
            }
        }

        // render the player
        p.renderMisses(g, this.getCameraX(), this.getCameraY());
        p.renderDialogue(g, this.getCameraX(), this.getCameraY());

        //render the objects that have been destroyed
        for (OtherObject o : this.getObjects()) {
            if (o.isOnMap()
                    && (!o.isDestroyed() || (o.getTravelType() == OtherObject.TravelType.AIR))) {
                o.render(g, this.getCameraX(), this.getCameraY());
            }
        }

        //render the items which HAVE exploding animations
        for (Item i : this.getItems()) {
            if (i.isOnMap() && i.isDestroyed()) {
                i.renderOnMap(g, this.getCameraX(), this.getCameraY());
            }
        }

        //render the flying units
        for (Unit u : this.getAllOtherUnits()) {
            if (u.isOnMap() && u.isAlive() && u.isFlyingUnit()) {
                u.render(g, this.getCameraX(), this.getCameraY());
            }
        }

        // render the display names, health bars, dialogues, misses of flying units
        for (Unit u : this.getAllOtherUnits()) {
            if (u.isAlive() && u.isOnMap() && u.isFlyingUnit()) {
                u.renderDisplayNameAndHP(g, this.getCameraX(), this.getCameraY());
                u.renderMisses(g, this.getCameraX(), this.getCameraY());
                u.renderDialogue(g, this.getCameraX(), this.getCameraY());

            }
        }

        // render the weather
        this.getWeatherGen().render(g, this.getCameraX(), this.getCameraY());

    }

    /** Reset the world to make start a new game.
     * @param filename The file from which the map details should be loaded.
     * @throws SlickException 
     */
    public void resetWorld(RPG rpg, String filename, String err)
            throws SlickException {

        if (!rpg.getMenu().isError()) {

            boolean error = true;
            try {
                error = !FileIO.loadGame(rpg.getCurrentQuest().getWorld(), filename);
            } catch (XMLStreamException e) {
                rpg.getMenu().gameError(err);
            } catch (IOException e) {
                rpg.getMenu().gameError(err);
            } catch (ParserConfigurationException e) {
                rpg.getMenu().gameError(err);
            } catch (SAXException e) {
                rpg.getMenu().gameError(err);
            }

            if (error) {
                rpg.getMenu().gameError(err);
            } else {
                // focus the camera on the player
                this.moveCameraTo((float) this.getPlayer().getX(), (float) this.getPlayer().getY());
            }

        }

    }

    /** Clears and resets all the collections and units in the world.
     */
    public void clearObjects() {

        this.getItems().clear();
        this.getObjects().clear();
        this.getAllOtherUnits().clear();
        this.getDeadUnitsOnly().clear();
        Item.resetCount();
        Unit.resetCount();

    }

    /** Shows all allies on the map.
     */
    public void showAllAllies(int team) {

        this.getQuest().addMsgToStatus("Discovered allies.");
//    	Ally.setDiscovered(true);
        for (Unit u : this.getAllOtherUnits()) {
            if (u.getTeam() == team) {
                u.makeEntrance();
                u.setAlive(true);
                u.setOnMap(true);
            }
        }

    }

    /** Creates all the units on the map.
     */
    private void cacheMapDetails() {

        for (int x = 0; x < this.getMap().getWidth(); x++) {
            for (int y = 0; y < this.getMap().getHeight(); y++) {

                int tileID = this.getMap().getTileId(x, y, 0);
                int t = Integer.parseInt(this.getMap().getTileProperty(tileID, TERRAIN_PROPERTY, DEFAULT_TERRAIN));
                this.getTerrainType()[x][y] = t;

                boolean b = this.getMap().getTileProperty(tileID, BLOCK_PROPERTY, TILE_UNBLOCKED).equals(TILE_BLOCKED);
                Color c = FontLoader.getColor(this.getMap().getTileProperty(tileID, COLOR_PROPERTY, DEFAULT_COLOR));

                tileID = this.getMap().getTileId(x, y, 2);
                b |= this.getMap().getTileProperty(tileID, BLOCK_PROPERTY, TILE_UNBLOCKED).equals(TILE_BLOCKED);
                c = FontLoader.getColor(this.getMap().getTileProperty(tileID, COLOR_PROPERTY, DEFAULT_COLOR), c);

                tileID = this.getMap().getTileId(x, y, 3);
                b |= this.getMap().getTileProperty(tileID, BLOCK_PROPERTY, TILE_UNBLOCKED).equals(TILE_BLOCKED);
                c = FontLoader.getColor(this.getMap().getTileProperty(tileID, COLOR_PROPERTY, DEFAULT_COLOR), c);

                this.getBlocked()[x][y] = b;
                this.getTileColors()[x][y] = c;

            }
        }

    }

	/** Check the blocked status of the tile at a given point.
     * @param x The X coordinate (pixels).
     * @param y The Y coordinate (pixels).
     * @return 'true' if either the tile is blocked or point not on the world, else 'false'.
     */
	public boolean isBlocked(double x, double y, Unit unit)
	{
		// get the boundaries of the player
		double x0 = unit.getMinX(x), y0 = unit.getMinY(y);
		double x1 = unit.getMaxX(x), y1 = unit.getMaxY(y);
		
		// check if the point is within the map/world
		boolean isInside = (x0 >= 0) && (x1 <= this.getWorldWidth()) && (y0 >= 0) && (y1 <= this.getWorldHeight());
		if (!isInside) { return true; }
		
		// make sure flying units are in restricted regions (if restricted)
		if (unit.isFlyingUnit()) {
			int tileIndexX = (int) (x / this.getMap().getTileWidth()); 
	        int tileIndexY = (int) (y / this.getMap().getTileHeight());
	        int t = unit.getRestrictedTerrain();
	        int tMap = this.getTerrainType()[tileIndexX][tileIndexY];
			if (t == TERRAIN_ANY) {
				return false;
			} else {
				return ((t != tMap) && (tMap != TERRAIN_OBSTACLE));
			}
		}
		
		// check for god mode cheat
		if (unit.equals(this.getPlayer())) {
        	if (Cheats.isOnGodMode()) {
        		return false;
        	}
        }
		
		// get the indices of the tiles that correspond to the four vertices of the player sprite 
		int tileIndexLX = (int) (x0 / this.getMap().getTileWidth()); 
        int tileIndexLY = (int) (y0 / this.getMap().getTileHeight());
		int tileIndexUX = (int) (x1 / this.getMap().getTileWidth());
        int tileIndexUY = (int) (y1 / this.getMap().getTileHeight());
        
        // check if either of them are blocked by the map
        boolean isNotFree = this.getBlocked()[tileIndexLX][tileIndexLY];
        isNotFree |= this.getBlocked()[tileIndexLX][tileIndexUY];
        isNotFree |= this.getBlocked()[tileIndexUX][tileIndexLY];
        isNotFree |= this.getBlocked()[tileIndexUX][tileIndexUY];
        if (isNotFree) { return true; }
        
		boolean isBlockedByOtherUnit = false;
		for (Unit u : this.getAllOtherUnits()) {
			if (!unit.equals(u) && u.isAlive() && u.isOnMap() && !u.isFlyingUnit()) {
				isBlockedByOtherUnit |= u.isOccupied(x0,y0);
				isBlockedByOtherUnit |= u.isOccupied(x0,y1);
				isBlockedByOtherUnit |= u.isOccupied(x1,y0);
				isBlockedByOtherUnit |= u.isOccupied(x1,y1);
			}
		}
		if (isBlockedByOtherUnit) { return true; }
		
		boolean isBlockedByObject = false;
		for (OtherObject o : this.getObjects()) {
			if (o.isOnMap() && o.isBlocking()) {
				isBlockedByObject |= o.isOccupied(x0,y0);
				isBlockedByObject |= o.isOccupied(x0,y1);
				isBlockedByObject |= o.isOccupied(x1,y0);
				isBlockedByObject |= o.isOccupied(x1,y1);
			}
		}
		if (isBlockedByObject) { return true; }
		
		boolean isBlockedByPlayer = false;
		if (!unit.equals(this.getPlayer()) && this.getPlayer().isAlive()) {
			isBlockedByPlayer |= this.getPlayer().isOccupied(x0,y0);
			isBlockedByPlayer |= this.getPlayer().isOccupied(x0,y1);
			isBlockedByPlayer |= this.getPlayer().isOccupied(x1,y0);
			isBlockedByPlayer |= this.getPlayer().isOccupied(x1,y1);
		}
		if (isBlockedByPlayer) { return true; }
		
		return false;
		
	}
//    /** Check the blocked status of the tile at a given point.
//     * @param x The X coordinate (pixels).
//     * @param y The Y coordinate (pixels).
//     * @return 'true' if either the tile is blocked or point not on the world, else 'false'.
//     */
//    public boolean isBlocked(double x, double y, Unit unit) {
//        unit.moveTo(x, y);
//
//        // get the boundaries of the player
//        double x0 = unit.getMinX(), y0 = unit.getMinY();
//        double x1 = unit.getMaxX(), y1 = unit.getMaxY();
//
//        // check if the point is within the map/world
//        boolean isInside = (x0 >= 0) && (x1 <= this.getWorldWidth()) && (y0 >= 0) && (y1 <= this.getWorldHeight());
//        if (!isInside) {
//            return true;
//        }
//
//        // make sure flying units are in restricted regions (if restricted)
//        if (unit.isFlyingUnit()) {
//            int tileIndexX = (int) (x / this.getMap().getTileWidth());
//            int tileIndexY = (int) (y / this.getMap().getTileHeight());
//            int t = unit.getRestrictedTerrain();
//            int tMap = this.getTerrainType()[tileIndexX][tileIndexY];
//            if (t == TERRAIN_ANY) {
//                return false;
//            } else {
//                return ((t != tMap) && (tMap != TERRAIN_OBSTACLE));
//            }
//        }
//
//        // check for god mode cheat
//        if (unit.equals(this.getPlayer())) {
//            if (Cheats.isOnGodMode()) {
//                return false;
//            }
//        }
//
//        // get the indices of the tiles that correspond to the four vertices of the player sprite
//        int tileIndexLX = (int) (x0 / this.getMap().getTileWidth());
//        int tileIndexLY = (int) (y0 / this.getMap().getTileHeight());
//        int tileIndexUX = (int) (x1 / this.getMap().getTileWidth());
//        int tileIndexUY = (int) (y1 / this.getMap().getTileHeight());
//
//        // check if either of them are blocked by the map
//        boolean isNotFree = this.getBlocked()[tileIndexLX][tileIndexLY];
//        isNotFree |= this.getBlocked()[tileIndexLX][tileIndexUY];
//        isNotFree |= this.getBlocked()[tileIndexUX][tileIndexLY];
//        isNotFree |= this.getBlocked()[tileIndexUX][tileIndexUY];
//        if (isNotFree) {
//            return true;
//        }
//
//        for (Unit u : this.getAllOtherUnits()) {
//            if (!unit.equals(u) && u.isAlive() && u.isOnMap() && !u.isFlyingUnit()) {
//                if (unit.collides(u)) {
//                    return true;
//                }
//            }
//        }
//
//        for (OtherObject o : this.getObjects()) {
//            if (o.isOnMap() && o.isBlocking()) {
//                if (unit.collides(o)) {
//                    return true;
//                }
//            }
//        }
//
//        if (!unit.equals(this.getPlayer()) && this.getPlayer().isAlive()) {
//            if (unit.collides(this.getPlayer())) {
//                return true;
//            }
//        }
//
//
//        return false;
//
//    }

    /** Move the camera to a particular point (camera focuses to the center of the screen).
     *  If the camera reaches the edge of the map, the it becomes static and lets the player move around;
     *  this prevents black area to be printed on the sides.
     * @param x The X coordinate (pixels).
     * @param y The Y coordinate (pixels).
     */
    private void moveCameraTo(float x, float y) {

        // set the camera to the given point in the world (usually the player's position)
        int gap = (Panel.getPanelHeight() >= Panel.getPanelGap()) ? Panel.getPanelGap() : 0;
        this.setCameraX(x - (RPG.getScreenWidth() / 2));
        this.setCameraY(y - ((RPG.getScreenHeight() - Panel.getPanelHeight() - gap) / 2));

        // check if the camera displays any point which is outside the world
        float w = this.getWorldWidth() - RPG.getScreenWidth();
        float h = this.getWorldHeight() - (RPG.getScreenHeight() - Panel.getPanelHeight() + gap);
        if (this.getCameraX() < 0) {
            this.setCameraX(0);
        }
        if (this.getCameraX() > w) {
            this.setCameraX(w);
        }
        if (this.getCameraY() < 0) {
            this.setCameraY(0);
        }
        if (this.getCameraY() > h) {
            this.setCameraY(h);
        }

    }

    /** Move the camera relative to its current position (camera focuses to the center of the screen).
     * The camera becomes static just as in 'moveCameraTo' method, if it reaches the end of the map.
     * @param x The number of pixels in the x direction (pixels).
     * @param y The number of pixels in the y direction (pixels).
     */
    private void moveCamera(float x, float y) {

        // set the camera to the given point in the world (usually the player's position)
        this.moveCameraTo(this.getCameraX() + x, this.getCameraY() + y);

    }

    /** Slide the camera to a particular point in a smooth manner (camera focuses to the center of the screen).
     *  If the camera reaches the edge of the map, the it becomes static and lets the player move around;
     *  this prevents black area to be printed on the sides.
     * @param x The X coordinate (pixels).
     * @param y The Y coordinate (pixels).
     * @param delta Time passed since last frame (milliseconds).
     */
    protected void slideCameraTo(float x, float y, int delta) {

        float dirx = x - this.getCameraX(), diry = y - this.getCameraY();
        this.moveCamera(dirx * CAMERA_SLIDE_SPEED_X * delta, diry * CAMERA_SLIDE_SPEED_X * delta);

    }

    /* Getters and Setters beyond this point */
    /** Get the X coordinate of the camera.
     * @return The X coordinate (pixels).
     */
    public float getCameraX() {
        return this.cameraX;
    }

    /** Set the X coordinate of the camera.
     * @param x The X coordinate (pixels).
     */
    private void setCameraX(float x) {
        this.cameraX = x;
    }

    /** Get the Y coordinate of the camera.
     * @return The Y coordinate (pixels).
     */
    public float getCameraY() {
        return this.cameraY;
    }

    /** Set the Y coordinate of the camera.
     * @param y The Y coordinate (pixels).
     */
    private void setCameraY(float y) {
        this.cameraY = y;
    }

    /** Get the screen width associated with the world (pixels).
     * @return The screen width.
     */
    private int getWorldWidth() {
        return this.getMap().getWidth() * this.getMap().getTileWidth();
    }

    /** Get the screen height associated with the world (pixels).
     * @return The screen height.
     */
    private int getWorldHeight() {
        return this.getMap().getHeight() * this.getMap().getTileHeight();
    }

    private boolean[][] getBlocked() {
        return this.blockedTiles;
    }

    public Color[][] getTileColors() {
        return this.tileColors;
    }

    public int[][] getTerrainType() {
        return this.tileTerrain;
    }

    public void setPlayer(Player player) {
        if (this.getPlayer() != null)
        {
            this.getAllUnits().remove(this.getPlayer());
        }
        this.player = player;
        this.getAllUnits().add(player);
    }

    public Player getPlayer() {
        return this.player;
    }

    public void addOtherUnit(Unit unit) {
        this.getAllOtherUnits().add(unit);
        this.getAllUnits().add(unit);
    }

    public void addObject(OtherObject obj) {
        this.getObjects().add(obj);
    }

    public void addItem(Item item) {
        this.getItems().add(item);
    }

    public ArrayList<Item> getItems() {
        return this.items;
    }

    public ArrayList<Unit> getAllOtherUnits() {
        return this.units;
    }

    public ArrayList<Unit> getAllUnits() {
        return this.allUnits;
    }

    public ArrayList<Unit> getDeadUnitsOnly() {
        return this.dead;
    }

    public ArrayList<OtherObject> getObjects() {
        return this.objects;
    }

    private void setMap(String map, String pathTiles)
            throws SlickException {
        this.map = new TiledMap(map, pathTiles);
    }

    public TiledMap getMap() {
        return this.map;
    }

    public Weather getWeatherGen() {
        return this.weather;
    }

    public PathFinder getFinder() {
        return this.finder;
    }

    public Quest getQuest() {
        return this.quest;
    }

    public boolean isOnScreen(double x, double y, double width, double height) {
        double w = width / 2, h = height / 2;
        boolean dirx = (x - w) >= 0 && (x + w) <= RPG.getScreenWidth();
        boolean diry = (y - h) >= 0 && (y + h) <= RPG.getScreenHeight();
        return dirx && diry;
    }

    public boolean isInsideMap(double x, double y, double width, double height) {
        double w = width / 2, h = height / 2;
        boolean dirx = (x - w) >= 0 && (x + w) <= this.getWorldWidth();
        boolean diry = (y - h) >= 0 && (y + h) <= this.getWorldHeight();
        return dirx && diry;
    }

    public void attackUnitsOnGround(double x, double y, double damageRange, double maxDamage, int damageTime) {

        for (Unit u : this.getAllOtherUnits()) {
            if (u.isKillable() && !u.isFlyingUnit()) {
                double dist2 = Unit.calculateDistanceSqrd(x, y, u.getX(), u.getY());
                double range = Unit.Squared(damageRange);
                if (dist2 <= range) {
                    u.getInjured((int) (maxDamage * (1 - Math.sqrt(dist2 / range))), damageTime);
                }
            }
        }

        Player p = this.getPlayer();
        double dist2 = Unit.calculateDistanceSqrd(x, y, p.getX(), p.getY());
        double range = Unit.Squared(damageRange);
        if (dist2 <= range) {
            p.getInjured((int) (maxDamage * (1 - Math.sqrt(dist2 / range))), damageTime);
        }

    }

    public void playMusic() {
        if (RPG.getSoundSettings().isPlayingSounds() && RPG.getSoundSettings().isBackgroundMusic()) {
            SoundLoader.getGameBgMusic().play(1f, RPG.getSoundSettings().getBackgroundMusicVolume());
        }
    }

    public void stopMusic() {
        SoundLoader.getGameBgMusic().stop();
    }

    @Override
    public boolean blocked(PathFindingContext context, int x, int y) {

        if (x < 0 || x >= this.getWidthInTiles() || y < 0 || y >= this.getHeightInTiles()) {
            return true;
        }
        return this.getBlocked()[x][y];

    }

    @Override
    public float getCost(PathFindingContext context, int x, int y) {
        return 1;
    }

    @Override
    public int getHeightInTiles() {
        return this.getMap().getWidth();
    }

    @Override
    public int getWidthInTiles() {
        return this.getMap().getHeight();
    }

    @Override
    public void pathFinderVisited(int x, int y) {
//		visited[x][y] = true;
    }
}
